/** 
 * Tray.java
 * PROJECT 3: Sliding Block Puzzle Solver
 * @author Jonathan Kotker	cs61b-ar
 * @author Wei Zhong Yeh	cs61b-av
 * 
 * The tray that contains blocks. Blocks in this tray are generally non-overlapping except in 
 * temporary trays used to explore possible moves. It does not store its dimensions, since this 
 * information is constant and needs not remain inside the tray.
 */

import java.util.*;

public class Tray {
	
	protected HashSet <Block> myBlocks; 
	// REMOVE!
	// private float myHeuristic;
	private Tray myParent;
	private int [ ] myBlockMove;
	private SortedBlocks mySortedBlocks;

	public Tray ( ) {
		this.myBlocks = new HashSet <Block> ( );
		this.myParent = null;
		// this.myDiscriminant = 0.0f;
		this.myBlockMove = new int [4];
		// this.mySortedBlocks = new SortedBlocks ( );
	}
	
	/**
	 * Constructor to construct a new tray resulting from moving a single block in one direction
	 * by one unit from an old tray
	 * @param oldTray	tray where move was made
	 * @param oldBlock	block before the move
	 * @param newBlock	block after the move
	 */
	public Tray (Tray oldTray, Block oldBlock, Block newBlock, Goal goalConfig) {
		// instantiate the collections of this tray
		this.myBlocks = new HashSet <Block> ( );
		this.myBlockMove = new int [4];
		// set parent of this tray
		this.myParent = oldTray;

		// add all the blocks from the old tray to this tray, replacing the old block with the new block
		this.myBlocks.addAll (oldTray.myBlocks);
		this.myBlocks.remove (oldBlock);
		this.myBlocks.add (newBlock);
		
		//this.mySortedBlocks = new SortedBlocks (oldTray.mySortedBlocks, oldBlock, newBlock);
		
		// REMOVE!
		// this.myDiscriminant = this.generateHeuristic (goalConfig);
				
		// store the coordinates of the previous block position and of the new block position
		this.myBlockMove [0] = (oldBlock.getCoordinates ( )) [0];
		this.myBlockMove [1] = (oldBlock.getCoordinates ( )) [1];
		this.myBlockMove [2] = (newBlock.getCoordinates ( )) [0];
		this.myBlockMove [3] = (newBlock.getCoordinates ( )) [1];
	}
	
	/**
	 * Adds a block object to this tray's list of blocks
	 * @param block 	a block object
	 * @param rowPos	a positive integer representing the row position of the upper left corner of the block
	 * @param colPos	a positive integer representing the column position of the upper left corner of the block
	 */
	public void addBlock (Block block) {
		this.myBlocks.add (block);
		//this.mySortedBlocks.add (block);
	}
	
	// REMOVE!
	/*public void setHeuristic (Goal goalConfig) {
	    this.myHeuristic = this.generateHeuristic (goalConfig);
	}
	
	
	public double getHeuristic ( ) {
		return this.myHeuristic;
	}

	
	private float generateHeuristic (Goal goalConfig) {
		// temporary iterator to move through the blocks of the goal configuration
		Iterator <Block> goalBlockIterator = goalConfig.blockIterator ( );
		// temporary copy of the list of the blocks of this tray configuration
		HashSet <Block> blocks = this.generateBlockListCopy ( );
		// temporary iterator to move through the blocks of this tray configuration
		Iterator <Block> blockIterator;
		// temporary variable to store the blocks of this tray configuration, as received
		Block currentBlock;
		// temporary variable to store the blocks of the goal configuration, as received
		Block goalBlock;
		// temporary variable to store the resultant heuristic of this tray configuration
		float result = 0.0f;

		// iterate through the blocks of the goal tray configuration to determine the heuristic
		while (goalBlockIterator.hasNext ( )) {
			goalBlock = goalBlockIterator.next ( );
			/// iterate through the blocks of this tray configuration
			blockIterator = blocks.iterator ( );
			while (blockIterator.hasNext ( )) {
				currentBlock = blockIterator.next ( );
				if ((currentBlock.getLength ( ) == goalBlock.getLength ( )) &&
					(currentBlock.getWidth ( )) == goalBlock.getWidth ( )) {
					// temporary variables to determine if other blocks matching the dimensions
					// of this goal block have better Manhattan distances, by storing the minimum
					// Manhattan distance discovered so far, and the block with the minimum Manhattan distance
					float minMD = Tray.getManhattanDistance (currentBlock.getCoordinates ( ), goalBlock.getCoordinates ( ));
					Block minMDBlock = currentBlock;
					// go through the other blocks to see if any other block has a better Manhattan distance
					while (blockIterator.hasNext ( )) {
						currentBlock = blockIterator.next ( );
						if ((currentBlock.getLength ( ) == goalBlock.getLength ( )) &&
							(currentBlock.getWidth ( )) == goalBlock.getWidth ( )) {
							// find the Manhattan distance of this block and compare
							float tempMD = Tray.getManhattanDistance (currentBlock.getCoordinates ( ), goalBlock.getCoordinates ( ));
							if (tempMD < minMD) {
								minMD = tempMD;
								minMDBlock = currentBlock;
							}
						}
					}
					// add the minimum Manhattan distance found
					result += minMD;
					// remove the block from the copy of the list of blocks of this tray configuration,
					// since one matching block has been found
					blocks.remove (minMDBlock);
				}
			} 
		}
		
		return result;
	}
	
	private HashSet <Block> generateBlockListCopy ( ) {
		HashSet <Block> result = new HashSet <Block> ( );
		result.addAll (this.myBlocks);
		return result;
	}
	
	*//**
	 * Generates the Manhattan distance between two blocks
	 * @param one		One set of coordinates for one block
	 * @param other		One set of coordinates for another block
	 * @return			The Manhattan distance between the two blocks
	 *//*
	private static int getManhattanDistance (int [ ] one, int [ ] other) {
		return (Math.abs (one [0] - other [0]) + Math.abs (one [1] - other [1]));
	}*/
	
	public Iterator <Block> blockIterator ( ) {
		return this.myBlocks.iterator ( );
	}
	
	public int [ ] getBlockMove ( ) {
		return this.myBlockMove;
	}

	public String toString ( ) {
		StringBuilder result = new StringBuilder ( );
		Iterator <Block> blocks = this.myBlocks.iterator ( );
		Block nextBlock;
		while (blocks.hasNext ( )) {
			nextBlock = blocks.next ( );
			result.append ("\n").append (nextBlock.toString ( ));
		}
		return result.toString ( );
	}
	
	public boolean equals (Object otherTray) {
		return this.myBlocks.equals (((Tray) otherTray).myBlocks);
	}
	
	// REMOVE!
	/*
	public int compareTo (Object argTray) {
		return new Double (this.myHeuristic).compareTo (new Double (((Tray) argTray).myHeuristic));
	}*/
	
	public int hashCode ( ) {
		int hashValue = 0;
		Iterator blocks = this.myBlocks.iterator ( );
		while (blocks.hasNext ( ))
			hashValue = hashValue + blocks.next ( ).hashCode ( );
		return hashValue;
	}
	
	/**
	 * Stores the trays along the path taken to arrive at this tray
	 * @param tray	tray to begin with
	 * @return		LinkedList containing before and after coordinates of blocks
	 */
	public static LinkedList <int [ ]> finalSolution (Tray tray) {
		LinkedList <int [ ]> result = new LinkedList <int [ ]> ( );
		for (Tray parent = tray; parent != null; parent = parent.myParent)
			result.add (parent.myBlockMove);
		return result;
	}	
	
	public SortedBlocks getSortedBlocks ( ) {
		return this.mySortedBlocks;
	}
	
	public boolean isOK (int trayLength, int trayWidth) {
		boolean blocksNonOverlapping = true;
		boolean blocksWithinTray = true;
		
		// making sure all blocks are nonoverlapping
		ArrayList <Block> blockList = new ArrayList <Block> ( );
		blockList.addAll (this.myBlocks);
		for (int i = 0; i < blockList.size ( ) && blocksNonOverlapping == true; i++)
			for (int j = 0; i < blockList.size ( ) && blocksNonOverlapping == true; j++)
				if (i != j && blockList.get (i).intersectsWith (blockList.get (j)))
					blocksNonOverlapping = false;
		
		// making sure all blocks are within the tray's boundaries
		Block tempBlock;
		Iterator <Block> blocks = this.blockIterator ( );
		while (blocks.hasNext ( ) && blocksWithinTray == true) {
			tempBlock = blocks.next ( );
			int [ ] coordinates = tempBlock.getCoordinates ( );
			if ((coordinates [0] < 0) || (coordinates [1] < 0) ||
				((coordinates [0] + tempBlock.getLength ( )) > trayLength) ||
				((coordinates [1] + tempBlock.getWidth ( )) > trayWidth))
				blocksWithinTray = false;
		}
		
		if (! (blocksNonOverlapping && blocksWithinTray))
			throw new IllegalStateException ( );
		return true;
	}
}